package suncertify.db;

import static java.util.logging.Level.INFO;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.logging.Logger;

import com.bodgittandscarper.broker.server.Database;
import com.bodgittandscarper.broker.types.DatabaseSchema;
import com.bodgittandscarper.broker.types.Record;
import com.bodgittandscarper.broker.types.RecordMod;
import com.bodgittandscarper.broker.types.TypeFactory;

/**
 * Main Data class that implements all of the methods defined in the <code>DBMain</code> interface and maintains the 
 * business model.
 * 
 * @author Morten Rummelhoff
 *
 */
public class Data implements DBMain {

	private Logger log = Logger.getLogger(getClass().getName());
	
	private final Object recordLockingMutex = new Object();
	
	private Database database;
	private DatabaseSchema databaseSchema;


	/**
	 * Constructor that takes a initialized {@link Database} as an argument.
	 * 
	 * @param database The Database to be used. The {@link Database} shall be initialized and ready to use.
	 */
	public Data(Database database)  {
		this.database = database;
		this.databaseSchema = database.getDatabaseSchema();
	}

	/**
	 * Gets the {@link DatabaseSchema} from the {@link Database}.
	 * @return the {@link DatabaseSchema} used in the {@link Database}.
	 */
	public DatabaseSchema getDatabaseSchema() {
		return databaseSchema;
	}
	
	/**
	 * Creates a new Record in the Database with the specified data.
	 * <p>
	 * First check if a Record already exist with the specified data. If it does throw a {@link DuplicateKeyException}.
	 * <p>
	 * Then check if a invalid Record exist in the database. If it does, reuse it.
	 * If no record can be reused a new Record is created with a Record number size matching the record list size.
	 * 
	 * @param data the Record data for the new created Record. The data array length must match the exact number of
	 * fields in a record.
	 * 
	 * @return The record number for the newly created record
	 * @throws DuplicateKeyException If a Record already exist with exact field match. Each field is evaluated.
	 */
	@Override
	public int create(String[] data) throws DuplicateKeyException {

		//First adjust lenght of record fields to comply with database structure.
		database.adjustRecordFieldsToComplyDBStructure(data);

		boolean exactRecordFound = false;
	
		for (int i = 0; i < database.getNumberOfRecords(); i++) {
			Record record = database.getRecord(i);

			//check if valid
			if (record.isValid() == false)
				continue;

			int fieldNr = 0;
			boolean recordFieldsMatch = true;
			
			while (recordFieldsMatch && fieldNr < databaseSchema.getDatabaseFieldCount()) {
				String recordField = record.getRecordFields()[fieldNr];
				String newRecordField = data[fieldNr];
				
				if (!recordField.equalsIgnoreCase(newRecordField)) {
					recordFieldsMatch = false;
				}
				
				fieldNr++;
					
			}
			
			if (recordFieldsMatch) {
				exactRecordFound = true;
				break;
			}
			
		}
		
		//check if matching record already exist
		if (exactRecordFound){
			//there are matching records in db. Throw a DuplicateKeyException
			throw new DuplicateKeyException("Duplicate record exist in database.");
		}

		RecordMod newRecord = null;
		
		//check if a invalid record exist in the list. If it does reuse it.
		for (int recNo = 0; recNo < database.getNumberOfRecords(); recNo++) {

			//check if not valid
			if (!database.getRecord(recNo).isValid()) {
				//record marked not valid, reuse it!
				newRecord = database.getRecord(recNo);

				newRecord.setValid(true);
				newRecord.updateRecordFields(data);
				newRecord.unlock();

				break;
			}

		}
		//check if newRecord is null, i.e no record could be reused. Then create new one and add it to list.
		if (newRecord == null) {

			int recNo = database.getNumberOfRecords();

			newRecord = TypeFactory.getInstance().createRecordMod(recNo, true, false, data);
			database.addRecord(newRecord);

		}

		//write to database file
		database.persistRecord(newRecord);

		//return the number of the new record
		return newRecord.getRecordNumber();
	}

	/**
	 * Deletes a {@link Record} making it available for reuse. 
	 * The deleted record is marked as invalid. 
	 * <p>
	 * Note that the Record must be locked in order to delete it.<br>
	 * Any waiting clients that waits for a Record to be unlocked are notified about the change
	 * This is done to release any client threads that may wait for that particularly record.	
	 *  
	 * @param recNo The number of the Record to be deleted.
	 * @throws RecordNotFoundException If the Record does not exist for the given Record number.
	 */
	@Override
	public void delete(int recNo) throws RecordNotFoundException {
		if (!database.doesRecodExist(recNo)) {
			throw new RecordNotFoundException("No Record exist for Record Number: " + recNo);
		}
		
		RecordMod record = database.getRecord(recNo);

		//check if the record is locked. Don't delete an unlocked record.
		if (record.isLocked()) {
			//set validflag to false so it can be reused.  
			record.setValid(false);
		
			//persist on file
			database.persistRecord(record);
		
		}
		
		//notify all possible clients about locked record has been deleted.
		synchronized (recordLockingMutex) {
			recordLockingMutex.notifyAll();
		}
		
	}

	/**
	 * Finds and returns an array containing records that match specified criteria. 
	 * Field n in the database file is described by
	 * criteria[n]. A null value in criteria[n] matches any field
	 * value. A non-null  value in criteria[n] matches any field
	 * value that begins with criteria[n]. (For example, "Fred"
	 * matches "Fred" or "Freddy".)
	 * <br>
	 * Note that lengths of specified criteria array must match length of fields in database. 
	 * 
	 * @param criteria The criteria defined by a String array. 
	 * 
	 * @return An Array containing record numbers that match the specified criteria. 
	 * @throws RecordNotFoundException If no {@link Record} match specified criteria.
	 */
	@Override
	public int[] find(String[] criteria) throws RecordNotFoundException {
		ArrayList<Integer> recordMatchList = new ArrayList<Integer>();

		if (criteria != null) {

			label: recordloop:
				for (int i = 0; i < database.getNumberOfRecords(); i++) {
					Record record = database.getRecord(i);

					//check if valid
					if (record.isValid() == false)
						continue;

					boolean recordMatches = false;

					for(int ii = 0; ii < databaseSchema.getDatabaseFieldCount(); ii++) {

						if (criteria.length > ii) {

							String fieldCriteria = criteria[ii];
							String recordField = record.getRecordFields()[ii];

							//check if criteria is defined for field. If not, make it a match so far. 
							//Continue look for next field in record
							if (fieldCriteria == null) {
								recordMatches = true;
							} 
							//criteria is defined for field, check if it matches field(startswith)
							//Continue look for next field in record
							else if (recordField.startsWith(fieldCriteria)) {
								recordMatches = true;
							}
							//criteria is defined, but it does not match field. Skip record regardless of other field in record, 
							//I.E. continue to next record.
							else {
								recordMatches = false;
								continue recordloop;
							}

						}

					}

					if (recordMatches) {
						recordMatchList.add(i);
					}
				}	
		}

		//check if find criteria resulted in any match
		if (recordMatchList.size() == 0) {
			//according to interface throw a RecordNotFoundException
			throw new RecordNotFoundException("No record was found matching with: " + Arrays.toString(criteria));
		}


		int[] result = new int[recordMatchList.size()];
		for (int i = 0; i < result.length;i++)
			result[i] = recordMatchList.get(i);

		return result;
	}

	/**
	 * Determines if a record is currently locked. Returns true if the
	 * record is locked, false otherwise.
	 * 
	 * @param recNo The recNo to check for.
	 * 
	 * @return A boolean value telling if the specified {@link Record} is locked. 
	 * @throws RecordNotFoundException If no {@link Record} could be found with the specified recNo.
	 */
	@Override
	public boolean isLocked(int recNo) throws RecordNotFoundException {
		if (!database.doesRecodExist(recNo))
			throw new RecordNotFoundException("No Record exist for Record Number: " + recNo);

		//check if record is already locked, then wait until record has been unlocked.
		RecordMod recordMod = database.getRecord(recNo);

		return recordMod.isLocked();
	}

	/**
	 * Locks a record so that it can only be updated or deleted by this client.
	 * If the specified record is already locked, the current client thread gives up
	 * the CPU and consumes no CPU cycles until the record is unlocked or deleted.
	 * <br>
	 * If the record is deleted a {@link RecordNotFoundException} will be 
	 * thrown to any other waiting client threads.
	 * 
	 * @param recNo The record to be locked.
	 * @throws RecordNotFoundException If no {@link Record} could be found with specified recNo. 
	 */
	@Override
	public void lock(int recNo) throws RecordNotFoundException {
		if (!database.doesRecodExist(recNo))
			throw new RecordNotFoundException("No Record exist for Record Number: " + recNo);

		//check if record is already locked, then wait until record has been unlocked.
		RecordMod recordMod = database.getRecord(recNo);

		while (recordMod.isLocked()) {
			
			//check if record has been deleted by another client-> valid flag set to false. 
			if (recordMod.isValid() == false) {
				//Record has been deleted by another client, inform this client by throwing a 
				//RecordNotFoundException
				throw new RecordNotFoundException("Locked record does not exist anymore.");
			}
			
			if (log.isLoggable(INFO))
				log.log(INFO, "Record already locked: " + recordMod);

			//Record already locked by another user. Then wait for record is unlocked.
			synchronized (recordLockingMutex) {
				try {
					recordLockingMutex.wait();
				} catch (InterruptedException e) {
					//ignore
				}
			}
		}

		recordMod.lock();	

		if (log.isLoggable(INFO))
			log.log(INFO, "Locked Record: " + recordMod);

	}

	/**
	 * Reads a record from the database. Returns an array where each
	 * element is a record value.
	 * 
	 * @param recNo The Record to read.
	 * 
	 * @return A field array containing the Records data.
	 * @throws RecordNotFoundException If no {@link Record} exist with the specified recNo.
	 */
	@Override
	public String[] read(int recNo) throws RecordNotFoundException {

		if (!database.doesRecodExist(recNo)){
			throw new RecordNotFoundException("No Record exist for Record Number: " + recNo);
		}
		else {
			return database.getRecord(recNo).getRecordFields();
		}

	}

	/**
	 * Releases a lock on a Record. Any waiting client threads are thereafter notified.
	 * 
	 * @param recNo The Record to unlock.
	 * @throws RecordNotFoundException If no {@link Record} exist for the specified recNo.
	 */
	@Override
	public void unlock(int recNo) throws RecordNotFoundException {
		if (!database.doesRecodExist(recNo))
			throw new RecordNotFoundException("No Record exist for Record Number: " + recNo);

		RecordMod recordMod = database.getRecord(recNo);

		recordMod.unlock();

		if (log.isLoggable(INFO))
			log.log(INFO, "Unlocked Record: " + recordMod);

		//notify all waiting clients
		synchronized (recordLockingMutex) {
			recordLockingMutex.notifyAll();
		}

	}

	/**
	 * Modifies the fields of a record. The new value for field n 
	 * appears in data[n].
	 * <br>
	 * Nothing is done if Record is not locked.
	 * 
	 * @param recNo The Record to be updated.
	 * @param data New value for Record. Note that length of data must match number of fields in Record.
	 * 
	 * @throws RecordNotFoundException If no {@link Record} exist for the specified recNo.
	 */
	@Override
	public void update(int recNo, String[] data) throws RecordNotFoundException {
		if (!database.doesRecodExist(recNo))
			throw new RecordNotFoundException("No Record exist for Record Number: " + recNo);

		//get record
		RecordMod recordMod = database.getRecord(recNo);

		//check if record is locked, if not, don't update record.
		if (recordMod.isLocked()) {

			//update record with new datafields
			recordMod.updateRecordFields(data);

			//persist to file
			database.persistRecord(recordMod);

		}

	}

}
